import re
from datetime import datetime

from django.db.models import QuerySet

import orjson

from ...models import EventDefinition, SchemaPropertyGroupProperty
from .base import EventDefinitionGenerator


class GolangGenerator(EventDefinitionGenerator):
    def generator_version(self) -> str:
        return "1.0.0"

    def language_name(self) -> str:
        return "Go"

    def generate(
        self, event_definitions: QuerySet[EventDefinition], schema_map: dict[str, list[SchemaPropertyGroupProperty]]
    ) -> str:
        """Generate complete Go package code for all event definitions"""
        now = datetime.now().isoformat()

        # Check if any property uses DateTime type to see if we need to import the "time" package.
        imports = []
        for event_def in event_definitions:
            properties = schema_map.get(str(event_def.id), [])
            if any(prop.property_type == "DateTime" for prop in properties):
                imports.append('\t"time"')
                break
        imports.append('\t"github.com/posthog/posthog-go"')
        imports_section = "\n".join(imports)

        event_blocks: list[str] = []
        for event_def in event_definitions:
            properties = schema_map.get(str(event_def.id), [])
            if not properties:
                # Generate minimal function for events without schema
                event_block = self._generate_event_without_properties(event_def.name)
                event_blocks.append(event_block)
                continue
            event_block = self._generate_event_with_properties(event_def.name, properties)
            event_blocks.append(event_block)

        header = f"""// Code generated by PostHog - DO NOT EDIT.
//
// Generated at: {now}
// Generator version: {self.generator_version()}
//
// This package provides type-safe helper functions to build posthog.Capture
// structs for your defined events. It provides:
// - A `<EventName>FromBase` function to allow full control over the `posthog.Capture` object.
// - A `<EventName>` function providing sane defaults for the `posthog.Capture` object.
//
// Optional properties can be passed through the `Option` functions / argument.
//
// If this code fails to compile:
// 1. Check your event/property names in PostHog.
// 2. Avoid special characters, reserved keywords, or names starting with numbers.
// 3. Use snake_case or kebab-case for best results.
// 4. If the above is not possible, please let us know and (for now) manually update the generated code whilst we work on a fix.

package typed

import (
{imports_section}
)

"""
        usage_guide = """// USAGE GUIDE:
//
// 1. Please store this file in a folder called `typed` (e.g. `./src/lib/typed`).
// 2. Import this package inside your code.
// 3. Make use of the typed capture functions. Every event has 2 sets of functions:
//    a) func <EventName>Capture
//    b) func <EventName>CaptureFromBase
//    generally we would recommend to use option A, unless you want to exercise full control over the underlying
//    `posthog.Capture` model.
//
//    Optional properties can be passed through the option pattern, where all available options are type hinted
//    as: <EventName>Option.
//    Finally, any untyped optional parameters can be passed through the `<EventName>WithExtraProps` function.
// 4. Pass the typed object to `posthog.Enqueue`. For example:
// ```
// err := posthog.Enqueue(typed.UserSignedUpCapture("user_456", "user@example.com", "premium"))
// ```
"""

        return header + "\n".join(event_blocks) + "\n" + usage_guide

    def _generate_event_without_properties(self, event_name: str) -> str:
        """Generate a minimal capture function for events without defined properties"""
        event_name = self._escape_go_string(event_name)
        base_name = self._to_go_func_name(event_name)
        func_name = f"{base_name}Capture"
        func_name_from_base = f"{func_name}FromBase"

        return f"""// {func_name} creates a capture for the {event_name} event.
// This event has no defined schema properties.
func {func_name}(distinctId string, properties ...posthog.Properties) posthog.Capture {{
\tprops := posthog.Properties{{}}
\tfor _, p := range properties {{
\t\tfor k, v := range p {{
\t\t\tprops[k] = v
\t\t}}
\t}}

\treturn posthog.Capture{{
\t\tDistinctId: distinctId,
\t\tEvent:      {event_name},
\t\tProperties: props,
\t}}
}}

// {func_name_from_base} creates a posthog.Capture for the {event_name} event
// starting from an existing base capture. The event name is overridden, and
// any additional properties can be passed via the properties parameter.
func {func_name_from_base}(base posthog.Capture, properties ...posthog.Properties) posthog.Capture {{
\tprops := posthog.Properties{{}}
\tfor _, p := range properties {{
\t\tfor k, v := range p {{
\t\t\tprops[k] = v
\t\t}}
\t}}

\tbase.Event = {event_name}
\tif base.Properties == nil {{
\t\tbase.Properties = posthog.Properties{{}}
\t}}
\tbase.Properties = base.Properties.Merge(props)

\treturn base
}}

"""

    def _generate_event_with_properties(self, event_name: str, properties: list[SchemaPropertyGroupProperty]) -> str:
        """Generate complete code block for an event with schema properties"""
        event_name = self._escape_go_string(event_name)
        base_name = self._to_go_func_name(event_name)
        option_type_name = f"{base_name}Option"

        # Split properties into required and optional
        required_props = sorted([p for p in properties if p.is_required], key=lambda p: p.name)
        optional_props = sorted([p for p in properties if not p.is_required], key=lambda p: p.name)

        block_lines: list[str] = [
            f"// {option_type_name} configures optional properties for a {event_name} capture.",
            f"type {option_type_name} func(*posthog.Capture)\n",
        ]

        # Generate option functions for optional properties
        used_function_names: set[str] = set()
        for prop in optional_props:
            option_func = self._generate_option_function(
                event_name, base_name, option_type_name, prop, used_function_names
            )
            block_lines.append(option_func)

        # Always generate WithExtraProps
        func_name_extra = self._get_unique_name(f"{base_name}WithExtraProps", used_function_names)
        block_lines.append(
            f"""// {func_name_extra} adds additional properties to a {event_name} event.
func {func_name_extra}(props posthog.Properties) {option_type_name} {{
\treturn func(c *posthog.Capture) {{
\t\tif c.Properties == nil {{
\t\t\tc.Properties = posthog.Properties{{}}
\t\t}}
\t\tfor k, v := range props {{
\t\t\tc.Properties[k] = v
\t\t}}
\t}}
}}
"""
        )

        # Generate main Capture function
        capture_func = self._generate_capture_function(event_name, base_name, option_type_name, required_props)
        block_lines.append(capture_func)

        # Generate CaptureFromBase function
        capture_from_base_func = self._generate_capture_from_base_function(
            event_name, base_name, option_type_name, required_props
        )
        block_lines.append(capture_from_base_func)

        return "\n".join(block_lines)

    def _generate_option_function(
        self,
        event_name: str,
        base_name: str,
        option_type_name: str,
        prop: SchemaPropertyGroupProperty,
        used_function_names: set[str],
    ) -> str:
        """Generate a With* option function for an optional property"""
        param_type = self._map_property_type(prop.property_type)
        prop_name = self._escape_go_string(prop.name)
        param_name = self._to_go_param_name(prop_name)
        suffix = self._to_pascal_name(prop_name)
        func_name = self._get_unique_name(f"{base_name}With{suffix}", used_function_names)

        return f"""// {func_name} sets the {prop_name} property on a {event_name} event.
func {func_name}({param_name} {param_type}) {option_type_name} {{
\treturn func(c *posthog.Capture) {{
\t\tif c.Properties == nil {{
\t\t\tc.Properties = posthog.Properties{{}}
\t\t}}
\t\tc.Properties[{prop_name}] = {param_name}
\t}}
}}
"""

    def _generate_capture_function(
        self, event_name: str, base_name: str, option_type_name: str, required_props: list[SchemaPropertyGroupProperty]
    ) -> str:
        func_name = f"{base_name}Capture"

        # Build parameters list
        props_init_lines = []
        params = ["distinctId string"]
        used_param_names: set[str] = {"distinctId"}

        for prop in required_props:
            prop_name = self._escape_go_string(prop.name)
            param_name = self._get_unique_name(self._to_go_param_name(prop_name), used_param_names)
            props_init_lines.append(f"\t\t{prop_name}: {param_name},")

            param_type = self._map_property_type(prop.property_type)
            params.append(f"{param_name} {param_type}")

        params_str = ",\n\t".join(params)

        return f"""// {func_name} is a wrapper for the {event_name} event.
// It manages the creation of the `posthog.Capture`. If you need control over this, please make use of
// the {func_name}FromBase function.
// Required properties from the schema are explicit parameters; optional properties
// should be passed via {base_name}With* option functions.
func {func_name}(
\t{params_str},
\toptions ...{option_type_name},
) posthog.Capture {{
\tprops := posthog.Properties{{
{chr(10).join(props_init_lines)}
\t}}

\tc := posthog.Capture{{
\t\tDistinctId: distinctId,
\t\tEvent:      {event_name},
\t\tProperties: props,
\t}}

\tfor _, opt := range options {{
\t\topt(&c)
\t}}

\treturn c
}}
"""

    def _generate_capture_from_base_function(
        self, event_name: str, base_name: str, option_type_name: str, required_props: list[SchemaPropertyGroupProperty]
    ) -> str:
        func_name = f"{base_name}CaptureFromBase"

        # Build parameters list
        params = ["base posthog.Capture"]
        props_init_lines = []
        used_param_names: set[str] = {"base"}

        for prop in required_props:
            prop_name = self._escape_go_string(prop.name)
            param_name = self._get_unique_name(self._to_go_param_name(prop_name), used_param_names)
            props_init_lines.append(f"\t\t{prop_name}: {param_name},")

            param_type = self._map_property_type(prop.property_type)
            params.append(f"{param_name} {param_type}")

        params_str = ",\n\t".join(params)

        return f"""// {func_name} creates a posthog.Capture for the {event_name} event
// starting from an existing base capture. The event name is overridden, and
// required properties from the schema are merged on top. Optional properties
// should be passed via {base_name}With* option functions.
func {func_name}(
\t{params_str},
\toptions ...{option_type_name},
) posthog.Capture {{
\tprops := posthog.Properties{{
{chr(10).join(props_init_lines)}
\t}}

\tbase.Event = {event_name}
\tif base.Properties == nil {{
\t\tbase.Properties = posthog.Properties{{}}
\t}}
\tbase.Properties = base.Properties.Merge(props)

\tfor _, opt := range options {{
\t\topt(&base)
\t}}

\treturn base
}}
"""

    def _map_property_type(self, property_type: str) -> str:
        type_map = {
            "String": "string",
            "Numeric": "float64",
            "Boolean": "bool",
            "DateTime": "time.Time",
            "Array": "[]interface{}",
            "Object": "map[string]interface{}",
        }
        return type_map.get(property_type, "interface{}")

    def _to_go_func_name(self, event_name: str) -> str:
        """
        Convert event name to exported Go identifier.
        Examples: 'downloaded_file' -> 'DownloadedFile', 'user-signed_up' -> 'UserSignedUp'

        Note: It attempts to make the parameters type-safe on an best effort basis
        """
        # Split on non-alphanumeric characters
        parts = re.split(r"[^0-9A-Za-z]+", event_name)
        # Capitalize each part
        camel = "".join(p[:1].upper() + p[1:] for p in parts if p)

        # Ensure it's a valid identifier
        if not camel:
            camel = "Event"

        # Ensure it starts with a letter
        if not camel[0].isalpha():
            camel = "Event" + camel

        return camel

    def _to_go_param_name(self, prop_name: str) -> str:
        """
        Convert property name to Go parameter name (camelCase).
        Examples: 'file_name' -> 'fileName', 'user-id' -> 'userId'

        Note: It attempts to make the parameters type-safe on an best effort basis
        """
        GO_RESERVED_KEYWORDS = {
            "break",
            "case",
            "chan",
            "const",
            "continue",
            "default",
            "defer",
            "else",
            "fallthrough",
            "for",
            "func",
            "go",
            "goto",
            "if",
            "import",
            "interface",
            "map",
            "package",
            "range",
            "return",
            "select",
            "struct",
            "switch",
            "type",
            "var",
        }

        parts = re.split(r"[^0-9A-Za-z]+", prop_name)
        parts = [p for p in parts if p]
        if not parts:
            return "value"

        if parts[0][0].isdigit():
            parts[0] = "param" + parts[0]

        # First part lowercase, rest capitalized
        first = parts[0].lower()
        rest = "".join(p[:1].upper() + p[1:].lower() for p in parts[1:] if p)
        param_name = first + rest

        if param_name in GO_RESERVED_KEYWORDS:
            param_name += "_"

        return param_name

    def _to_pascal_name(self, prop_name: str) -> str:
        """
        Convert property name to PascalCase for option function suffixes.
        Examples: 'file_name' -> 'FileName', 'user-id' -> 'UserId'
        """
        parts = re.split(r"[^0-9A-Za-z]+", prop_name)
        pascal = "".join(p[:1].upper() + p[1:] for p in parts if p)
        return pascal or "Prop"

    def _get_unique_name(self, base_name: str, used_names: set[str]) -> str:
        """
        Generate a unique name by adding numeric suffixes if there are collisions.

        Examples:
            - First usage: "fileName" → "fileName"
            - Second usage: "fileName" → "fileName2"
            - Third usage: "fileName" → "fileName3"
        """
        if base_name not in used_names:
            used_names.add(base_name)
            return base_name

        # Add numeric suffix for collision
        i = 2
        while f"{base_name}{i}" in used_names:
            i += 1
        unique_name = f"{base_name}{i}"
        used_names.add(unique_name)
        return unique_name

    def _escape_go_string(self, value: str) -> str:
        """
        Escape a string value for use in Go string literals.
        """
        return orjson.dumps(value).decode("utf-8")
